// Copyright 2008 David Mitchell
// Released under the MIT License

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Reflection;
using System.Runtime.InteropServices;

namespace NSDuctTape
{
	/// <summary>
	/// A wrapper around an instance of NSInvocation.
	/// </summary>
	internal sealed class Invocation
	{
		/// <summary>
		/// Initializes a new instance of Invocation.
		/// </summary>
		/// <param name="nativeInvocation">A pointer to the native NSInvocation object.</param>
		public Invocation(IntPtr nativeInvocation)
		{
			if (nativeInvocation == IntPtr.Zero)
				throw new ArgumentException("nativeObject", "nativeObject cannot be equal to IntPtr.Zero.");
			_nativeInvocation = nativeInvocation;
		}
		
		public Invocation(IntPtr nativeInstance, IntPtr selector)
		{
			IntPtr methodSignature = ObjectiveCMethods.SendMessage(nativeInstance, Selectors.MethodSignatureForSelector, selector);
			_nativeInvocation = ObjectiveCMethods.SendMessage(Classes.Invocation, Selectors.InvocationWithMethodSignature, methodSignature);
			
			ObjectiveCMethods.SendMessage(_nativeInvocation, Selectors.SetSelector, selector);
			ObjectiveCMethods.SendMessage(_nativeInvocation, Selectors.SetTarget, nativeInstance);
		}

		/// <summary>
		/// Gets the selector with which this instance is associated.
		/// </summary>
		public string Selector
		{
			get
			{
				return Marshal.PtrToStringAnsi(ObjectiveCMethods.SendMessage(_nativeInvocation, Selectors.Selector));
			}
		}
		
		public void Invoke(params object[] arguments)
		{
			IntPtr methodSignature = ObjectiveCMethods.SendMessage(_nativeInvocation, Selectors.MethodSignature);
			
			int numberOfArguments = ObjectiveCMethods.SendMessageWithInt32Result(methodSignature, Selectors.NumberOfArguments);
			if (numberOfArguments - 2 != arguments.Length)
				throw new ArgumentException("Incorrect number of arguments specified");
			
			List<HGlobal> globals = new List<HGlobal>(arguments.Length);
			try
			{
				int argumentIndex = 2;
				foreach (object argument in arguments)
				{
					Type type = argument.GetType();
					
					if (type == typeof(bool))
						globals.Add(Box((bool)argument == true ? (byte)1 : (byte)0));
					else if (type == typeof(byte))
						globals.Add(Box((byte)argument));
					else if (type == typeof(short))
						globals.Add(Box((short)argument));
					else if (type == typeof(int))
						globals.Add(Box((int)argument));
					else if (type == typeof(long))
						globals.Add(Box((long)argument));
					else if (type == typeof(float))
						globals.Add(Box((float)argument));
					else if (type == typeof(double))
						globals.Add(Box((double)argument));
					else if (type == typeof(IntPtr))
						globals.Add(Box((IntPtr)argument));
					else if (type == typeof(string))
						globals.Add(Box((string)argument));
					else if (type == typeof(Color))
						globals.Add(Box((Color)argument));
					else if (type == typeof(Instance))
						globals.Add(Box(((Instance)argument).ToIntPtr()));
					else
						globals.Add(Box(InstanceUtility.InstanceForObject(argument)));
				
					ObjectiveCMethods.SendMessage(_nativeInvocation, Selectors.SetArgumentAtIndex, globals[argumentIndex - 2].ToIntPtr(), argumentIndex);
				
					argumentIndex++;
				}
				
				ObjectiveCMethods.SendMessage(_nativeInvocation, Selectors.Invoke);
			}
			finally
			{
				foreach (HGlobal global in globals)
					global.Dispose();
			}
		}
		
		public TResult Invoke<TResult>(params object[] arguments)
		{
			IntPtr methodSignature = ObjectiveCMethods.SendMessage(_nativeInvocation, Selectors.MethodSignature);
			
			int numberOfArguments = ObjectiveCMethods.SendMessageWithInt32Result(methodSignature, Selectors.NumberOfArguments);
			if (numberOfArguments - 2 != arguments.Length)
				throw new ArgumentException("Incorrect number of arguments specified");
			
			List<HGlobal> globals = new List<HGlobal>(arguments.Length);
			try
			{
				int argumentIndex = 2;
				foreach (object argument in arguments)
				{
					Type type = argument.GetType();
					
					if (type == typeof(bool))
						globals.Add(Box((bool)argument == true ? (byte)1 : (byte)0));
					else if (type == typeof(byte))
						globals.Add(Box((byte)argument));
					else if (type == typeof(short))
						globals.Add(Box((short)argument));
					else if (type == typeof(int))
						globals.Add(Box((int)argument));
					else if (type == typeof(long))
						globals.Add(Box((long)argument));
					else if (type == typeof(float))
						globals.Add(Box((float)argument));
					else if (type == typeof(double))
						globals.Add(Box((double)argument));
					else if (type == typeof(IntPtr))
						globals.Add(Box((IntPtr)argument));
					else if (type == typeof(string))
						globals.Add(Box((string)argument));
					else if (type == typeof(Color))
						globals.Add(Box((Color)argument));
					else if (type == typeof(Instance))
						globals.Add(Box(((Instance)argument).ToIntPtr()));
					else
						globals.Add(Box(InstanceUtility.InstanceForObject(argument)));
				
					ObjectiveCMethods.SendMessage(_nativeInvocation, Selectors.SetArgumentAtIndex, globals[argumentIndex - 2].ToIntPtr(), argumentIndex);
				
					argumentIndex++;
				}
				
				ObjectiveCMethods.SendMessage(_nativeInvocation, Selectors.Invoke);
				
				using (HGlobal buffer = new HGlobal((int)ObjectiveCMethods.SendMessageWithUInt32Result(methodSignature, Selectors.MethodReturnLength)))
				{
					ObjectiveCMethods.SendMessage(_nativeInvocation, Selectors.GetReturnValue, buffer.ToIntPtr());
					TResult result;
					
					if (typeof(TResult) == typeof(bool))
						result = (TResult)(object)(Marshal.ReadByte(buffer.ToIntPtr()) != 0);
					else if (typeof(TResult) == typeof(byte))
						result = (TResult)(object)Marshal.ReadByte(buffer.ToIntPtr());
					else if (typeof(TResult) == typeof(short))
						result = (TResult)(object)Marshal.ReadInt16(buffer.ToIntPtr());
					else if (typeof(TResult) == typeof(int))
						result = (TResult)(object)Marshal.ReadInt32(buffer.ToIntPtr());
					else if (typeof(TResult) == typeof(long))
						result = (TResult)(object)Marshal.ReadInt64(buffer.ToIntPtr());
					else if (typeof(TResult) == typeof(float))
					{
						float[] array = new float[1];
						Marshal.Copy(buffer.ToIntPtr(), array, 0, 1);
						result = (TResult)(object)array[0];
					}
					else if (typeof(TResult) == typeof(double))
					{
						double[] array = new double[1];
						Marshal.Copy(buffer.ToIntPtr(), array, 0, 1);
						result = (TResult)(object)array[0];
					}
					else if (typeof(TResult) == typeof(IntPtr))
						result = (TResult)(object)Marshal.ReadIntPtr(buffer.ToIntPtr());
					else if (typeof(TResult) == typeof(string))
						result = (TResult)(object)NativeString.NativeStringToString(Marshal.ReadIntPtr(buffer.ToIntPtr()));
					else if (typeof(TResult) == typeof(Color))
						result = (TResult)(object)NativeColor.NativeColorToColor(Marshal.ReadIntPtr(buffer.ToIntPtr()));
					else if (typeof(TResult) == typeof(Instance))
						result = (TResult)(object)(new Instance(Marshal.ReadIntPtr(buffer.ToIntPtr())));
					else
						result = (TResult)InstanceUtility.ObjectForInstance(Marshal.ReadIntPtr(buffer.ToIntPtr()));
					
					return result;
				}
			}
			finally
			{
				foreach (HGlobal global in globals)
					global.Dispose();
			}
		}

		/// <summary>
		/// Gets the arguments associated with this invocation.
		/// </summary>
		/// <param name="method">Information about the expected parameters.</param>
		/// <returns>An array representing the arguments associated with this invocation.</returns>
		public object[] GetArguments(ParameterInfo[] parameters)
		{
			object[] arguments = new object[parameters.Length];
			int argumentIndex = 0;
			foreach (Type type in Enumerable.Select<ParameterInfo, Type>(parameters, delegate(ParameterInfo info) { return info.ParameterType; }))
			{
				if (type == typeof(bool))
					arguments[argumentIndex] = ArgumentToByte(argumentIndex + 2) != 0;
				else if (type == typeof(byte))
					arguments[argumentIndex] = ArgumentToByte(argumentIndex + 2);
				else if (type == typeof(short))
					arguments[argumentIndex] = ArgumentToInt16(argumentIndex + 2);
				else if (type == typeof(int))
					arguments[argumentIndex] = ArgumentToInt32(argumentIndex + 2);
				else if (type == typeof(long))
					arguments[argumentIndex] = ArgumentToInt64(argumentIndex + 2);
				else if (type == typeof(float))
					arguments[argumentIndex] = ArgumentToSingle(argumentIndex + 2);
				else if (type == typeof(double))
					arguments[argumentIndex] = ArgumentToDouble(argumentIndex + 2);
				else if (type == typeof(IntPtr))
					arguments[argumentIndex] = ArgumentToIntPtr(argumentIndex + 2);
				else if (type == typeof(string))
					arguments[argumentIndex] = ArgumentToString(argumentIndex + 2);
				else if (type == typeof(Color))
					arguments[argumentIndex] = ArgumentToColor(argumentIndex + 2);
				else if (type == typeof(Instance))
					arguments[argumentIndex] = new Instance(ArgumentToIntPtr(argumentIndex + 2));
				else
				{
					IntPtr nativeObject = ArgumentToIntPtr(argumentIndex + 2);
					if (nativeObject != IntPtr.Zero)
						arguments[argumentIndex] = InstanceUtility.ObjectForInstance(nativeObject);
				}

				argumentIndex++;
			}

			return arguments;
		}

		/// <summary>
		/// Sets the value that should be returned to the caller.
		/// </summary>
		/// <param name="value">The value that should be returned.</param>
		public void SetReturnValue(object value)
		{
			Type type = value != null ? value.GetType() : default(Type);
			IntPtr nativeObject = IntPtr.Zero;
			try
			{
				if (type == typeof(byte) || type == typeof(bool))
				{
					nativeObject = Marshal.AllocHGlobal(sizeof(byte));
					Marshal.WriteByte(nativeObject, (byte)value);
				}
				else if (type == typeof(short))
				{
					nativeObject = Marshal.AllocHGlobal(sizeof(short));
					Marshal.WriteInt16(nativeObject, (short)value);
				}
				else if (type == typeof(int))
				{
					nativeObject = Marshal.AllocHGlobal(sizeof(int));
					Marshal.WriteInt32(nativeObject, (int)value);
				}
				else if (type == typeof(long))
				{
					nativeObject = Marshal.AllocHGlobal(sizeof(long));
					Marshal.WriteInt64(nativeObject, (long)value);
				}
				else if (type == typeof(float))
				{
					nativeObject = Marshal.AllocHGlobal(sizeof(float));
					float[] array = new float[] { (float)value };
					Marshal.Copy(array, 0, nativeObject, 1);
				}
				else if (type == typeof(double))
				{
					nativeObject = Marshal.AllocHGlobal(sizeof(double));
					double[] array = new double[] { (double)value };
					Marshal.Copy(array, 0, nativeObject, 1);
				}
				else if (type == typeof(IntPtr))
				{
					nativeObject = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(IntPtr)));
					Marshal.WriteIntPtr(nativeObject, (IntPtr)value);
				}
				else if (type == typeof(string))
				{
					nativeObject = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(IntPtr)));
					Marshal.WriteIntPtr(nativeObject, NativeString.StringToNativeString(value as string));
				}
				else if (type == typeof(Color))
				{
					nativeObject = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(IntPtr)));
					Marshal.WriteIntPtr(nativeObject, NativeColor.ColorToNativeColor((Color)value));
				}
				else if (type == typeof(Instance))
				{
					nativeObject = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(IntPtr)));
					Marshal.WriteIntPtr(nativeObject, ((Instance)value).ToIntPtr());
				}

				if (nativeObject != IntPtr.Zero)
					ObjectiveCMethods.SendMessage(_nativeInvocation, Selectors.SetReturnValue, nativeObject);
			}
			finally
			{
				if (nativeObject != IntPtr.Zero)
					Marshal.FreeHGlobal(nativeObject);
			}
		}

		/// <summary>
		/// Gets an argument from the invocation and converts it to a byte.
		/// </summary>
		/// <param name="index">The index of the argument. Note that actual arguments start at index 2; the first two arguments are the receiver and selector, respectively.</param>
		/// <returns>The argument.</returns>
		private byte ArgumentToByte(int index)
		{
			using (HGlobal buffer = new HGlobal(sizeof(Byte)))
			{
				ObjectiveCMethods.SendMessage(_nativeInvocation, Selectors.GetArgumentAtIndex, buffer.ToIntPtr(), index);
				return Marshal.ReadByte(buffer.ToIntPtr());
			}
		}
		
		private HGlobal Box(byte value)
		{
			HGlobal buffer = new HGlobal(sizeof(Byte));
			Marshal.WriteByte(buffer.ToIntPtr(), value);
			return buffer;
		}

		/// <summary>
		/// Gets an argument from the invocation and converts it to an Int16.
		/// </summary>
		/// <param name="index">The index of the argument. Note that actual arguments start at index 2; the first two arguments are the receiver and selector, respectively.</param>
		/// <returns>The argument.</returns>
		private short ArgumentToInt16(int index)
		{
			using (HGlobal buffer = new HGlobal(sizeof(Int16)))
			{
				ObjectiveCMethods.SendMessage(_nativeInvocation, Selectors.GetArgumentAtIndex, buffer.ToIntPtr(), index);
				return Marshal.ReadInt16(buffer.ToIntPtr());
			}
		}

		private HGlobal Box(short value)
		{
			HGlobal buffer = new HGlobal(sizeof(Int16));
			Marshal.WriteInt16(buffer.ToIntPtr(), value);
			return buffer;
		}

		/// <summary>
		/// Gets an argument from the invocation and converts it to an Int32.
		/// </summary>
		/// <param name="index">The index of the argument. Note that actual arguments start at index 2; the first two arguments are the receiver and selector, respectively.</param>
		/// <returns>The argument.</returns>
		private int ArgumentToInt32(int index)
		{
			using (HGlobal buffer = new HGlobal(sizeof(Int32)))
			{
				ObjectiveCMethods.SendMessage(_nativeInvocation, Selectors.GetArgumentAtIndex, buffer.ToIntPtr(), index);
				return Marshal.ReadInt32(buffer.ToIntPtr());
			}
		}
		
		private HGlobal Box(int value)
		{
			HGlobal buffer = new HGlobal(sizeof(Int32));
			Marshal.WriteInt32(buffer.ToIntPtr(), value);
			return buffer;
		}

		/// <summary>
		/// Gets an argument from the invocation and converts it to an Int64.
		/// </summary>
		/// <param name="index">The index of the argument. Note that actual arguments start at index 2; the first two arguments are the receiver and selector, respectively.</param>
		/// <returns>The argument.</returns>
		private long ArgumentToInt64(int index)
		{
			using (HGlobal buffer = new HGlobal(sizeof(Int64)))
			{
				ObjectiveCMethods.SendMessage(_nativeInvocation, Selectors.GetArgumentAtIndex, buffer.ToIntPtr(), index);
				return Marshal.ReadInt64(buffer.ToIntPtr());
			}
		}
		
		private HGlobal Box(long value)
		{
			HGlobal buffer = new HGlobal(sizeof(Int64));
			Marshal.WriteInt64(buffer.ToIntPtr(), value);
			return buffer;
		}

		/// <summary>
		/// Gets an argument from the invocation and converts it to a Single.
		/// </summary>
		/// <param name="index">The index of the argument. Note that actual arguments start at index 2; the first two arguments are the receiver and selector, respectively.</param>
		/// <returns>The argument.</returns>
		private float ArgumentToSingle(int index)
		{
			using (HGlobal buffer = new HGlobal(sizeof(Single)))
			{
				ObjectiveCMethods.SendMessage(_nativeInvocation, Selectors.GetArgumentAtIndex, buffer.ToIntPtr(), index);
				float[] array = new float[1];
				Marshal.Copy(buffer.ToIntPtr(), array, 0, 1);
				return array[0];
			}
		}
		
		private HGlobal Box(float value)
		{
			HGlobal buffer = new HGlobal(sizeof(Single));
			float[] array = new float[] { value };
			Marshal.Copy(array, 0, buffer.ToIntPtr(), 1);
			return buffer;
		}

		/// <summary>
		/// Gets an argument from the invocation and converts it to a Double.
		/// </summary>
		/// <param name="index">The index of the argument. Note that actual arguments start at index 2; the first two arguments are the receiver and selector, respectively.</param>
		/// <returns>The argument.</returns>
		private double ArgumentToDouble(int index)
		{
			using (HGlobal buffer = new HGlobal(sizeof(Double)))
			{
				ObjectiveCMethods.SendMessage(_nativeInvocation, Selectors.GetArgumentAtIndex, buffer.ToIntPtr(), index);
				double[] array = new double[1];
				Marshal.Copy(buffer.ToIntPtr(), array, 0, 1);
				return array[0];
			}
		}
		
		private HGlobal Box(double value)
		{
			HGlobal buffer = new HGlobal(sizeof(Double));
			double[] array = new double[] { value };
			Marshal.Copy(array, 0, buffer.ToIntPtr(), 1);
			return buffer;
		}

		/// <summary>
		/// Gets an argument from the invocation and converts it to an IntPtr.
		/// </summary>
		/// <param name="index">The index of the argument. Note that actual arguments start at index 2; the first two arguments are the receiver and selector, respectively.</param>
		/// <returns>The argument.</returns>
		private IntPtr ArgumentToIntPtr(int index)
		{
			using (HGlobal buffer = new HGlobal(Marshal.SizeOf(typeof(IntPtr))))
			{
				ObjectiveCMethods.SendMessage(_nativeInvocation, Selectors.GetArgumentAtIndex, buffer.ToIntPtr(), index);
				return Marshal.ReadIntPtr(buffer.ToIntPtr());
			}
		}
		
		private HGlobal Box(IntPtr value)
		{
			HGlobal buffer = new HGlobal(Marshal.SizeOf(typeof(IntPtr)));
			Marshal.WriteIntPtr(buffer.ToIntPtr(), value);
			return buffer;
		}

		/// <summary>
		/// Gets an argument from the invocation and converts it to a String.
		/// </summary>
		/// <param name="index">The index of the argument. Note that actual arguments start at index 2; the first two arguments are the receiver and selector, respectively.</param>
		/// <returns>The argument.</returns>
		private string ArgumentToString(int index)
		{
			IntPtr nativeString = ArgumentToIntPtr(index);
			return NativeString.NativeStringToString(nativeString);
		}
		
		private HGlobal Box(string value)
		{
			IntPtr nativeString = NativeString.StringToNativeString(value);
			return Box(nativeString);
		}
		
		/// <summary>
		/// Gets an argument from the invocation and converts it to a System.Drawing.Color.
		/// </summary>
		/// <param name="index">The index of the argument. Note that actual arguments start at index 2; the first two arguments are the receiver and selector, respectively.</param>
		/// <returns>The argument.</returns>
		private Color ArgumentToColor(int index)
		{
			IntPtr nativeColor = ArgumentToIntPtr(index);
			return NativeColor.NativeColorToColor(nativeColor);
		}
		
		private HGlobal Box(Color value)
		{
			IntPtr nativeColor = NativeColor.ColorToNativeColor(value);
			return Box(nativeColor);
		}

		/// <summary>
		/// A pointer to the NSInvocation instance.
		/// </summary>
		readonly IntPtr _nativeInvocation;
	}
}
